using System;
using System.Diagnostics.Contracts;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

using E = System.Linq.Expressions.Expression;
using OE = ReactiveCode.Observables.Expressions.ObservableExpressionExtensions;

namespace ReactiveCode.Observables.Expressions
{
    using Util;
    using Util.Expressions;

    class SubjectConverter : ExpressionConverter
    {
        protected override Type GenericResultTypeDefinition
        {
            get { return typeof(ISubject<>); }
        }

        ObservableConverter _observableConverter = new ObservableConverter();

        public override Expression Convert(Expression expression, Expression stateDecorator)
        {
            _observableConverter._decorator = stateDecorator;
            try
            {
                return base.Convert(expression, stateDecorator);
            }
            finally
            {
                _observableConverter._decorator = null;
            }
        }

        E CreateObserver(LambdaExpression setter)
        {
            var valueType = setter.Parameters.Single().Type;
            if (setter.Body.Type != typeof(void))
                setter = E.Lambda(
                    E.Block(
                        setter.Body,
                        E.Empty()
                    ),
                    setter.Parameters);
            var observer = E.Call(typeof(Observer), "Create", new[] { valueType }, setter);
            return observer.Decorate(_decorator);
        }
        E VisitMember(E expression)
        {
            Contract.Requires(expression != null);

            var result = new List<E>();
            E state = expression;

            var args = expression.ExtractMemberArgs().ToArray();
            var vars = new List<ParameterExpression>();
            var argValues = new List<E>();
            var observableArgs = new List<E>();
            E hasValue = null;
            for(int i = 0; i < args.Length; i++)
            {
                var observableArg = _observableConverter.VisitInternal(args[i]);
                E value;
                if (observableArg.ExtractReturn(out value))
                {
                    Contract.Assume(value.ToString() == args[i].ToString());
                    argValues.Add(value);
                    observableArgs.Add(observableArg);
                }
                else
                {
                    var latest = observableArg.LatestContinuous();
                    var var = E.Variable(latest.Type, "v" + i);
                    vars.Add(var);
                    observableArgs.Add(var);

                    result.Add(E.Assign(var, latest));

                    var varValue = E.Property(var, "Value");
                    argValues.Add(varValue);
                    state = state.Replace(args[i], varValue);
                    Contract.Assume(state.Type == expression.Type);

                    hasValue = ExpressionUtil.SafeAndAlso(hasValue, E.Property(var, "HasValue"));
                }
            }
            var observable = _observableConverter.ConvertMember(expression, args, observableArgs.ToArray());
            
            var newValue = E.Parameter(expression.Type);
            E setterBody = E.Assign(state, newValue);
            if (hasValue != null)
                setterBody = E.IfThen(hasValue, setterBody);
            var observer = CreateObserver(E.Lambda(setterBody, newValue));

            var subject = E.Call(typeof(Subject), "Create", new[] { expression.Type }, observable, observer);

            result.Add(subject);
            if (result.Count == 1)
                return result[0];
            return E.Block(vars, result);
        }

        protected override E VisitIndex(IndexExpression exp)
        {
            return VisitMember(exp);
        }
        protected override E VisitMember(MemberExpression exp)
        {
            return VisitMember(exp);
        }
        protected override E VisitParameter(ParameterExpression exp)
        {
            var observable = OE.Return(exp);
            var newValue = E.Parameter(exp.Type);
            var observer = CreateObserver(E.Lambda(
                E.Assign(exp, newValue),
                newValue
            ));
            return E.Call(typeof(Subject), "Create", new[] { exp.Type }, observable, observer);
        }
    }
}